/**
 * Copyright 2011, Wannabet Developers
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.googlecode.wannabet.oauth
{
	import com.googlecode.wannabet.oauth.events.OAuthEvent;
	import com.googlecode.wannabet.oauth.sign.IOAuthSigner;
	import com.googlecode.wannabet.util.KeyValuePair;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.events.LocationChangeEvent;
	import flash.events.SecurityErrorEvent;
	import flash.geom.Rectangle;
	import flash.media.StageWebView;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.net.URLRequestHeader;
	import flash.net.URLRequestMethod;
	
	import mx.core.FlexGlobals;
	import mx.utils.UIDUtil;
	
	public class AbstractOAuth extends EventDispatcher implements IOAuth {
		
		private var consumer:OAuthConsumer = null;
		
		private var token:OAuthToken = null;
		
		private var oauthVerifier:String = null;
		
		public function AbstractOAuth(consumer:OAuthConsumer) {
			super();
			this.consumer = consumer;
		}
		
		public function authenticate():void {
			fetchOAuthGetRequestToken();
		}
		
		private function fetchOAuthGetRequestToken():void {
			// let's start the GetRequestToken call
			trace('Starting GetRequestToken call....');
			var additionalRequestParamTokens:Array = getAdditionRequetParamTokens();
			if(additionalRequestParamTokens == null) {
				additionalRequestParamTokens = new Array();
			}
			additionalRequestParamTokens.push(new KeyValuePair('oauth_callback', getCallbackURL()));
			
			const signedRequest:URLRequest = getSignedRequest(getHttpRequestMethod(), getRequestTokenURL(), additionalRequestParamTokens);
			
			const urlLoader:URLLoader = new URLLoader();
			urlLoader.addEventListener(Event.COMPLETE, requestTokenComplete);
			urlLoader.addEventListener(IOErrorEvent.IO_ERROR, handleError);
			urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
			
			urlLoader.load(signedRequest);
		}
		
		private function requestTokenComplete(event:Event):void {
			var urlLoader:URLLoader = event.target as URLLoader;
			
			// remove event listeners
			urlLoader.removeEventListener(Event.COMPLETE, requestTokenComplete);
			urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, handleError);
			urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
			
			trace('handle complete invoked.');
			trace('data received as: ' + urlLoader.data);
			
			// extract the oauth token and secret
			this.token = extractTokenAndSecret(urlLoader.data);
			
			trace(this.token);
			
			// start authorization workflow
			fetchOAuthAuthorizeToken();
		}
		
		private function handleError(event:IOErrorEvent):void {
			var urlLoader:URLLoader = event.target as URLLoader;
			
			trace("I/O Error: " + event.text + "\n" + urlLoader.data);
		}
		
		private function handleSecurityError(event:SecurityErrorEvent):void {
			var urlLoader:URLLoader = event.target as URLLoader;
			
			trace("Security Error: " + event.text + "\n" + urlLoader.data);
		}
		
		private function fetchOAuthAuthorizeToken():void {
			trace('Starting fetchOAuthAuthorizeToken call....');
			const webView:StageWebView = new StageWebView();
			webView.addEventListener("locationChange", onWebViewURLChange);	
			
			webView.stage = FlexGlobals.topLevelApplication.stage;
			webView.assignFocus();
			webView.viewPort = new Rectangle(0, 0, 480, 800);
			
			var url:String = getAuthorizeTokenURL() + "?oauth_token=" + this.token.key;
			webView.loadURL(url);	
		}
		
		private function onWebViewURLChange(event:LocationChangeEvent):void {
			const webView:StageWebView = event.target as StageWebView;
			if (webView.location.indexOf(getCallbackURL()) != -1) {
				const verifierSearch:String = "oauth_verifier=";
				const tokenSearch:String = "&oauth_token=";
				const location:String = webView.location;
				
				var startIndex:int = location.lastIndexOf(verifierSearch);
				var endIndex:int = location.lastIndexOf(tokenSearch);
				
				this.oauthVerifier = decodeURIComponent(location.substring(startIndex + verifierSearch.length, endIndex));
				const oauthToken:String = decodeURIComponent(location.substr(endIndex + tokenSearch.length));
				
				this.token = new OAuthToken(oauthToken, this.token.secret);
				trace('verifier: ' + this.oauthVerifier);
				trace('token: ' + this.token);
				
				webView.removeEventListener("locationChange", onWebViewURLChange);
				webView.dispose();
				
				fetchOAuthGetAccessToken();
			}
		}
		
		private function fetchOAuthGetAccessToken():void {
			trace('Starting fetchOAuthGetAccessToken call....');
			var urlRequest:URLRequest = getSignedRequest("GET", getAccessTokenURL());
			
			var urlLoader:URLLoader = new URLLoader();
			urlLoader.addEventListener(Event.COMPLETE, accessTokenComplete);
			urlLoader.addEventListener(IOErrorEvent.IO_ERROR, handleError);
			urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
			
			urlLoader.load(urlRequest);
		}
		
		private function accessTokenComplete(event:Event):void {
			var urlLoader:URLLoader = event.target as URLLoader;
			
			// remove event listeners
			urlLoader.removeEventListener(Event.COMPLETE, accessTokenComplete);
			urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, handleError);
			urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
			
			trace('handle complete invoked.');
			trace('data received as: ' + urlLoader.data);
			
			// extract the access token and secret
			this.token = extractTokenAndSecret(urlLoader.data);
			
			trace(this.token);
			
			// fire the completion event
			trace('request sucessfully authorized.');
			
			this.dispatchEvent(new OAuthEvent(OAuthEvent.COMPLETE));
		}
		
		/**
		 * The method is used to extract the oauth token and secret from the returned response.
		 */
		private function extractTokenAndSecret(data:String):OAuthToken {
			var splitArr:Array = data.split("&");
			var n:int = splitArr.length;
			
			var key:String = null;
			var secret:String = null;
			
			for (var i:int = 0; i < n; i++) {
				var element:Array = String(splitArr[i]).split("=");
				if (element[0] == "oauth_token") {
					key = decodeURIComponent(element[1]);
				} else if(element[0] == "oauth_token_secret") {
					secret = decodeURIComponent(element[1]);
				}
			}
			
			return new OAuthToken(key, secret);
		}
		
		private function getSignedRequest(requestMethod:String, url:String, additionalRequestParams:Array = null, requestHeaders:Array = null):URLRequest {
			var request:URLRequest = new URLRequest();
			
			var requestParams:Array = new Array();
			
			var currentDate:Date = new Date();
			var uuid:String = UIDUtil.getUID(currentDate);
			var timestamp:uint = currentDate.time / 1000;
			
			requestParams.push(new KeyValuePair('oauth_consumer_key', this.consumer.key));
			requestParams.push(new KeyValuePair('oauth_nonce', uuid));
			requestParams.push(new KeyValuePair('oauth_signature_method', 'HMAC-SHA1'));
			requestParams.push(new KeyValuePair('oauth_timestamp', new String(timestamp)));
			requestParams.push(new KeyValuePair('oauth_version', '1.0'));
			
			if(additionalRequestParams != null) {
				for each(var kvPair:KeyValuePair in additionalRequestParams) {
					requestParams.push(kvPair);
				}
			}
			
			if(this.token != null) {
				trace(this.token);
				requestParams.push(new KeyValuePair('oauth_token', this.token.key));
			}
			
			if(this.oauthVerifier != null) {
				requestParams.push(new KeyValuePair('oauth_verifier', this.oauthVerifier));
			}
			
			// oauth_signature has to be the last line
			
			var signableString:String = getSignableString(url, requestMethod, requestParams);
			var signature:String = getSigner().signRequest(signableString, this.consumer, this.token);
			trace('signature: ' + signature);
			
			requestParams.push(new KeyValuePair('oauth_signature', signature));
			
			// build up the authorization header
			var authHeader:String = "OAuth ";
			for each(var pair:KeyValuePair in requestParams) {
				if(pair.key.substr(0, 6) == 'oauth_') {
					authHeader += pair.key + '="' + encodeURIComponent(pair.value) + '", '; 
				}
			}
			// remove trailing comma and white space
			authHeader = authHeader.substring(0, authHeader.length - 2);
			
			request.requestHeaders.push(new URLRequestHeader('Authorization', authHeader));
			
			if(requestMethod == URLRequestMethod.POST) {
				request.contentType = 'application/x-www-form-urlencoded';
			} else if(requestMethod == URLRequestMethod.GET) {
				var paramString:String = getArrayAsString(requestParams, '&', true);
				if(paramString != null && paramString.length > 0) {
					url += "?" + paramString;
				}
			} else {
				request.data = new Object();
				for each(var kv:KeyValuePair in requestParams) {
					request.data[kv.key] = kv.value;
				}
			}
			
			if(requestHeaders != null) {
				for each(var header:KeyValuePair in requestHeaders) {
					request.requestHeaders.push(new URLRequestHeader(header.key, header.value));
				}
			}
			
			trace('URL: ' + url);
			request.url = url;
			
			trace('*** Request Headers');
			for each(var rh:URLRequestHeader in request.requestHeaders) {
				trace(rh.name + ": " + rh.value);
			}
			
			trace("Content-Type: " + request.contentType);
			
			return request;
		}
		
		protected function getSignableString(url:String, method:String, params:Array):String {
			var signableString:String = encodeURIComponent(method.toUpperCase());
			signableString += "&";
			signableString += encodeURIComponent(url);
			
			params.sortOn('key');
			
			var signableParameters:String = getArrayAsString(params, '&');
			
			if(signableParameters != null) {
				signableParameters = encodeURIComponent(signableParameters);
				trace('signable params: ' + signableParameters);
				signableString = signableString + "&" + signableParameters;
			}
			
			trace('signableString: ' + signableString);
			return signableString;
		}
		
		private function getArrayAsString(array:Array, delimiter:String, removeOAuthParams:Boolean = false):String {
			var value:String = "";
			
			for each(var kv:KeyValuePair in array) {
				if(!removeOAuthParams || kv.key.substr(0, 6) != 'oauth_') {
					value += kv.key + "=" + encodeURIComponent(kv.value) + delimiter;
				}
			}
			
			value = value.substr(0, value.length - delimiter.length);
			
			return value;
		}
		
		public function makeApiCall(url:String, requestParams:Array, requestHeaders:Array, completionFunction:Function = null, errorFunction:Function = null):void {
			// remove any previous instance of OAuth verifier string
			this.oauthVerifier = null;
			
			//			this.token = new OAuthToken('1/t5Hr8CsEV7Bss2sQP5ceLjwaSb4AGxIO90Ws3-6Z1nM', 'rlYZ36M19gFgK9KotxYhUwyD');
			
			// build the API request
			const signedRequest:URLRequest = getSignedRequest(getHttpRequestMethod(), url, requestParams, requestHeaders);
			const urlLoader:URLLoader = new URLLoader();
			urlLoader.addEventListener(Event.COMPLETE, apiCallComplete);
			urlLoader.addEventListener(IOErrorEvent.IO_ERROR, handleError);
			urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
			
			urlLoader.load(signedRequest);
		}
		
		private function apiCallComplete(event:Event):void {
			var urlLoader:URLLoader = event.target as URLLoader;
			
			// remove event listeners
			urlLoader.removeEventListener(Event.COMPLETE, apiCallComplete);
			urlLoader.removeEventListener(IOErrorEvent.IO_ERROR, handleError);
			urlLoader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, handleSecurityError);
			
			trace('API call data: ' + urlLoader.data);
			this.dispatchEvent(new OAuthEvent(OAuthEvent.COMPLETE, urlLoader.data));
		}
		
		protected function getAdditionRequetParamTokens():Array {
			return null;
		}
		
		protected function getRequestTokenURL():String {
			throw new Error('must be implemented in a child class.');
		}
		
		protected function getAuthorizeTokenURL():String {
			throw new Error('must be implemented in a child class.');
		}
		
		protected function getCallbackURL():String {
			throw new Error('must be implemented in a child class.');
		}
		
		protected function getAccessTokenURL():String {
			throw new Error('must be implemented in a child class.');
		}
		
		protected function getSigner():IOAuthSigner {
			throw new Error('must be implemented in a child class.');
		}
		
		protected function getHttpRequestMethod():String {
			throw new Error('must be implemented in a child class.');
		}
	}
}
